package com.tambapps.p2p.fandem

import com.gitee.wsl.android.ext.device.NetworkExt.privateNetworkIpAddressOrNull
import com.google.gson.Gson
import com.google.gson.JsonIOException
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import com.tambapps.p2p.fandem.exception.IncompatibleVersionException
import com.tambapps.p2p.speer.Peer
import com.tambapps.p2p.speer.datagram.DatagramSupplier
import com.tambapps.p2p.speer.datagram.MulticastDatagramPeer
import com.tambapps.p2p.speer.datagram.service.MulticastReceiverService
import com.tambapps.p2p.speer.datagram.service.PeriodicMulticastService
import com.tambapps.p2p.speer.io.Deserializer
import com.tambapps.p2p.speer.io.Serializer
import com.tambapps.p2p.speer.util.PeerUtils
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.InetAddress
import java.util.Arrays
import java.util.concurrent.ExecutorService
import java.util.concurrent.ScheduledExecutorService
import java.util.stream.Collectors

object Fandem {
    // using IPv6 because android multicast packet receiving doesn't work for IPv4 multicast addresses
    // (well at least I couldn't make it work) but it does work with an IPv6 multicast address
    val PEER_DISCOVERY_MULTICAST_ADDRESS = PeerUtils.getAddress("ff02::1")
    const val PEER_DISCOVERY_PORT = 50000
    private val GSON = Gson()
    private val SERIALIZER: Serializer<*> =
        Serializer { `object`: Any?, outputStream: OutputStream? ->
            try {
                val dataOutputStream = DataOutputStream(outputStream)
                dataOutputStream.writeUTF(GSON.toJson(`object`))
                dataOutputStream.flush()
            } catch (e: JsonIOException) {
                throw IOException(e)
            }
        }
    const val VERSION = "3.1"
    val VERSION_FIELDS = Arrays.stream(VERSION.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }
        .toTypedArray())
        .mapToInt { s: String -> s.toInt() }
        .toArray()

    fun <T> deserializer(clazz: Class<T>?): Deserializer<T> {
        return Deserializer { inputStream: InputStream? ->
            try {
                return@Deserializer GSON.fromJson<T>(DataInputStream(inputStream).readUTF(), clazz)
            } catch (e: JsonIOException) {
                throw IOException(e)
            } catch (e: JsonSyntaxException) {
                throw IOException(e)
            }
        }
    }

    fun <T> serializer(): Serializer<T> {
        return SERIALIZER as Serializer<T>
    }

    fun multicastService(
        executor: ScheduledExecutorService?
    ): PeriodicMulticastService<List<SenderPeer>> {
        return PeriodicMulticastService(
            executor,
            PEER_DISCOVERY_MULTICAST_ADDRESS,
            PEER_DISCOVERY_PORT,
            senderPeersSerializer(),
            ArrayList()
        )
    }

    @Throws(IOException::class)
    fun senderPeersSupplier(): DatagramSupplier<List<SenderPeer>> {
        return DatagramSupplier(multicastReceiverDatagram(), senderPeersDeserializer())
    }

    @Throws(IOException::class)
    fun multicastReceiverDatagram(): MulticastDatagramPeer {
        val datagramPeer = MulticastDatagramPeer(PEER_DISCOVERY_PORT)
        datagramPeer.joinGroup(PEER_DISCOVERY_MULTICAST_ADDRESS)
        return datagramPeer
    }

    fun senderPeersSerializer(): Serializer<List<SenderPeer>> {
        return serializer()
    }

    fun senderPeersDeserializer(): Deserializer<List<SenderPeer>> {
        val type = object : TypeToken<List<SenderPeer?>?>() {}.type
        return Deserializer<List<SenderPeer>> { `is`: InputStream? ->
            val senderPeers = GSON.fromJson<List<SenderPeer>>(DataInputStream(`is`).readUTF(), type)
            // filter own sender peers (if multicasting any)
            val address: InetAddress = privateNetworkIpAddressOrNull?:return@Deserializer emptyList()
            senderPeers.stream()
                .filter { p: SenderPeer -> p.address != address }
                .collect(Collectors.toList<SenderPeer>())
        }
    }

    fun senderPeersReceiverService(
        executorService: ExecutorService?,
        listener: MulticastReceiverService.DiscoveryListener<List<SenderPeer>>?
    ): MulticastReceiverService<List<SenderPeer>> {
        return MulticastReceiverService(
            executorService, PEER_DISCOVERY_MULTICAST_ADDRESS,
            PEER_DISCOVERY_PORT, senderPeersDeserializer(), listener
        )
    }

    @Throws(IOException::class)
    fun findAvailableSendingPeer(): Peer {
        return Peer.findAvailablePeer(SenderPeer.DEFAULT_PORT)
    }

    fun parsePeerFromHexString(hexString: String): Peer {
        require(
            !(hexString.length != 8 && hexString.length != 10 ||
                    !hexString.chars()
                        .allMatch { c: Int -> Character.isDigit(c) || c >= 'A'.code && c <= 'F'.code || c >= 'a'.code && c <= 'f'.code })
        ) { String.format("'%s' is malformed", hexString) }
        val address = IntArray(4)
        for (i in address.indices) {
            address[i] = hexString.substring(i * 2, i * 2 + 2).toInt(16)
        }
        val port =
            if (hexString.length == 8) SenderPeer.DEFAULT_PORT else SenderPeer.DEFAULT_PORT + hexString.substring(
                8,
                10
            ).toInt(16)
        return Peer.of(PeerUtils.getAddress(Arrays.stream(address)
            .mapToObj { i: Int -> Integer.toString(i) }
            .collect(Collectors.joining("."))), port)
    }

    fun toHexString(peer: Peer): String {
        val ipHex = toHexString(peer.address)
        return if (peer.port == SenderPeer.DEFAULT_PORT) ipHex else ipHex +
                toHexString(peer.port - SenderPeer.DEFAULT_PORT)
    }

    fun toHexString(address: String?): String {
        return toHexString(PeerUtils.getAddress(address))
    }

    /**
     * Returns the hex string of the given ip
     *
     * @param address the address
     * @return the hex string of the given ip
     */
    fun toHexString(address: InetAddress?): String {
        return Arrays.stream(
            PeerUtils.toString(address).split("\\.".toRegex()).dropLastWhile { it.isEmpty() }
                .toTypedArray())
            .map { s: String -> s.toInt() }
            .map( { obj: Int -> toHexString(obj) })
            .collect(Collectors.joining())
    }

    fun isCorrectPeerKey(peerKey: String?): Boolean {
        var peerKey = peerKey ?: return false
        peerKey = peerKey.uppercase()
        if (peerKey.length != 8 && peerKey.length != 10) {
            return false
        }
        for (i in 0 until peerKey.length) {
            val c = peerKey[i]
            if (!(c in 'A'..'F' || c in '0'..'9')) {
                return false
            }
        }
        return true
    }

    fun toHexString(i: Int): String {
        val n = Integer.toHexString(i).uppercase()
        return if (n.length > 1) n else "0$n"
    }

    @Throws(IncompatibleVersionException::class)
    fun checkVersionCompatibility(version: String) {
        val fieldsString =
            version.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
        if (fieldsString.size != 2) {
            throw IncompatibleVersionException()
        }
        val fields = IntArray(2)
        for (i in fields.indices) {
            try {
                fields[i] = fieldsString[i].toInt()
            } catch (e: NumberFormatException) {
                throw IncompatibleVersionException()
            }
        }
        if (VERSION_FIELDS[0] != fields[0]) {
            throw IncompatibleVersionException()
        }
    }
}
